﻿//-----------------------------------------------------------------------
// <copyright file="HttpCompress.ashx.cs" company="CMSN Software">
//    Copyright © 2010  CMSN Software
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see http://www.gnu.org/licenses.
// </copyright>
//-----------------------------------------------------------------------

namespace CMSN.Software.Tutorials.HowToCompressCssAndJs
{
    using System;
    using System.Globalization;
    using System.IO;
    using System.IO.Compression;
    using System.Text;
    using System.Web;
    using System.Web.Caching;

    /// <summary>
    /// compact combine cache and compress css and JavaScript files at runtime.
    /// </summary>
    public class HttpCompress : IHttpHandler
    {
        /// <summary>
        /// Default cache duration
        /// </summary>
        private static readonly TimeSpan CacheDuration = TimeSpan.FromDays(30);

        /// <summary>
        /// file names from query string
        /// </summary>
        private string fileNames;

        /// <summary>
        /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"/> instance.
        /// </summary>
        /// <returns>true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.</returns>
        public bool IsReusable
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface.
        /// </summary>
        /// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
        public void ProcessRequest(HttpContext context)
        {
            string cacheKeyName = context.Request.Url.PathAndQuery;
            this.fileNames = context.Request.QueryString["files"] ?? string.Empty;
            string[] relativeFiles = this.fileNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            string[] absoluteFiles = new string[relativeFiles.Length];
            StringBuilder combinedScript = new StringBuilder();
            bool isCss;

            if (relativeFiles.Length > 0 && relativeFiles[0].EndsWith(".css", StringComparison.OrdinalIgnoreCase))
            {
                isCss = true;
                context.Response.ContentType = "text/css";
            }
            else
            {
                isCss = false;
                context.Response.ContentType = "application/x-javascript";
            }

            for (int i = 0; i < relativeFiles.Length; i++)
            {
                string absoluteFile = context.Server.MapPath("~/" + relativeFiles[i]);
                combinedScript.Append(MinifyContent(absoluteFile, isCss));
                absoluteFiles[i] = absoluteFile;
            }

            if (context.Cache[CacheKey(cacheKeyName, CompressionType(context))] != null)
            {
                byte[] responseBytes = context.Cache[CacheKey(cacheKeyName, CompressionType(context))] as byte[];
                this.WriteContent(responseBytes, context, CompressionType(context), cacheKeyName, absoluteFiles);
            }
            else
            {
                CacheDependency dependency = new CacheDependency(absoluteFiles);
                string compressionType = CompressionType(context);
                if (compressionType == "deflate")
                {
                    using (MemoryStream stream = new MemoryStream())
                    {
                        using (StreamWriter writer = new StreamWriter(new DeflateStream(stream, CompressionMode.Compress), Encoding.UTF8))
                        {
                            writer.Write(combinedScript);
                        }

                        byte[] buffer = stream.ToArray();

                        context.Cache.Insert(
                            CacheKey(cacheKeyName, compressionType),
                            buffer,
                            dependency,
                            System.Web.Caching.Cache.NoAbsoluteExpiration,
                            CacheDuration);

                        this.WriteContent(buffer, context, compressionType, cacheKeyName, absoluteFiles);
                    }
                }
                else if (compressionType == "gzip")
                {
                    using (MemoryStream stream = new MemoryStream())
                    {
                        using (StreamWriter writer = new StreamWriter(new GZipStream(stream, CompressionMode.Compress), Encoding.UTF8))
                        {
                            writer.Write(combinedScript);
                        }

                        byte[] buffer = stream.ToArray();
                        context.Cache.Insert(
                            CacheKey(cacheKeyName, compressionType),
                            buffer,
                            dependency,
                            System.Web.Caching.Cache.NoAbsoluteExpiration,
                            CacheDuration);
                        this.WriteContent(buffer, context, compressionType, cacheKeyName, absoluteFiles);
                    }
                }
                else
                {
                    context.Cache.Insert(
                        CacheKey(cacheKeyName, compressionType),
                        combinedScript,
                        dependency,
                        System.Web.Caching.Cache.NoAbsoluteExpiration,
                        CacheDuration);

                    context.Response.AddHeader("Content-Length", combinedScript.Length.ToString(CultureInfo.InvariantCulture));
                    context.Response.Write(combinedScript);
                }
            }
        }

        /// <summary>
        /// Gets the compression type that the browser supports
        /// </summary>
        /// <param name="context">The context.</param>
        /// <returns>
        /// none, deflate, or gzip
        /// </returns>
        protected static string CompressionType(HttpContext context)
        {
            string compressionType = "none";
            string encodingTypes = string.Empty;
            encodingTypes = context.Request.Headers["Accept-Encoding"];

            if (!string.IsNullOrEmpty(encodingTypes))
            {
                encodingTypes = encodingTypes.ToLower(CultureInfo.InvariantCulture);
                if (context.Request.Browser.Browser == "IE")
                {
                    if (context.Request.Browser.MajorVersion < 6)
                    {
                        compressionType = "none";
                    }
                    else if (context.Request.Browser.MajorVersion == 6 &&
                        !string.IsNullOrEmpty(context.Request.ServerVariables["HTTP_USER_AGENT"]) &&
                        context.Request.ServerVariables["HTTP_USER_AGENT"].Contains("EV1"))
                    {
                        compressionType = "none";
                    }
                }

                if (encodingTypes.Contains("deflate") || encodingTypes.Contains("*"))
                {
                    compressionType = "deflate";
                }
                else if (encodingTypes.Contains("gzip") || encodingTypes.Contains("x-gzip"))
                {
                    compressionType = "gzip";
                }
            }

            return compressionType;
        }

        /// <summary>
        /// Cache key for the HttpCompress module.
        /// </summary>
        /// <param name="key">default cache key.</param>
        /// <param name="compressionType">Type of the compression.</param>
        /// <returns>Cache key for the HttpCompress module</returns>
        protected static string CacheKey(string key, string compressionType)
        {
            return "HttpCompress." + key + "." + compressionType;
        }

        /// <summary>
        /// Sets the response cache.
        /// </summary>
        /// <param name="response">response of the current context</param>
        /// <param name="files">absolute file paths.</param>
        protected void SetResponseCache(HttpResponse response, string[] files)
        {
            response.AddFileDependencies(files);
            HttpCachePolicy browserCache = response.Cache;
            DateTime modifiedTime = DateTime.Now;
            browserCache.SetCacheability(HttpCacheability.Public);
            browserCache.VaryByParams["files"] = true;
            browserCache.VaryByParams["v"] = true;
            browserCache.SetOmitVaryStar(true);
            browserCache.SetExpires(modifiedTime.AddDays(7));
            browserCache.SetValidUntilExpires(true);
            browserCache.SetLastModified(modifiedTime);
            browserCache.SetETagFromFileDependencies();
            browserCache.SetLastModifiedFromFileDependencies();
        }

        /// <summary>
        /// Writes the bytes.
        /// </summary>
        /// <param name="fileContent">The bytes.</param>
        /// <param name="context">The context.</param>
        /// <param name="compressionType">Type of the compression.</param>
        /// <param name="cacheKey">The cache key.</param>
        /// <param name="files">The files.</param>
        protected void WriteContent(byte[] fileContent, HttpContext context, string compressionType, string cacheKey, string[] files)
        {
            HttpResponse response = context.Response;

            this.SetResponseCache(response, files);

            if (compressionType != "none")
            {
                response.AddHeader("Content-encoding", compressionType);
                response.OutputStream.Write(fileContent, 0, fileContent.Length);
            }
            else
            {
                string uncompressedScript = context.Cache[CacheKey(cacheKey, compressionType)].ToString();
                response.AddHeader("Content-Length", uncompressedScript.Length.ToString(CultureInfo.InvariantCulture));
                response.Write(uncompressedScript);
            }
        }

        /// <summary>
        /// minify individual file content.
        /// </summary>
        /// <param name="file">css or JavaScript file name.</param>
        /// <param name="isCss">if set to <c>true</c> [is CSS].</param>
        /// <returns>
        /// minified file content
        /// </returns>
        private static string MinifyContent(string file, bool isCss)
        {
            string compactContent = string.Empty;
            if (File.Exists(file))
            {
                using (StreamReader reader = new StreamReader(file))
                {
                    compactContent = StripWhitespace(reader.ReadToEnd(), isCss);
                }
            }

            return compactContent;
        }

        /// <summary>
        /// Strips the whitespace.
        /// </summary>
        /// <param name="rowContent">Content of the row.</param>
        /// <param name="isCss">if set to <c>true</c> [is CSS].</param>
        /// <returns>minified file content</returns>
        private static string StripWhitespace(string rowContent, bool isCss)
        {
            if (isCss)
            {
                rowContent = StyleSheetMinifier.CssMinify(rowContent);
            }
            else
            {
                JavaScriptMinifier javaScriptMinify = new JavaScriptMinifier();
                rowContent = javaScriptMinify.MinifyJavaScript(rowContent);
            }

            return rowContent;
        }
    }
}